package com.espirit.eap.util;

import static org.json.JSONObject.NULL;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.struts2.ServletActionContext;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.security.Context;
import com.googlecode.cswish.security.SafeManager;
import com.googlecode.cswish.security.auth.UserInfo;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.util.FunctionCallback;
import com.googlecode.cswish.util.Functions;
import com.googlecode.cswish.util.OgnlExUtil;
import com.googlecode.cswish.util.StringUtil;
import com.googlecode.cswish.util.StringUtil.MappingProperty;
import com.googlecode.cswish.util.bean.ModelScanner;
import com.googlecode.cswish.util.bean.ModelScanner.ScannerCallback;
import com.googlecode.cswish.util.bean.ModelScanner.ScannerFlag;
import com.googlecode.cswish.util.bean.ModelUtil;
import com.googlecode.cswish.util.bean.ScannerContext;
import com.opensymphony.xwork2.util.ValueStack;

@Service
public class JSONUtil {
	private static final Logger logger = Logger.getLogger(JSONUtil.class);
	
	@Resource
	protected OgnlExUtil ognlExUtil;
	
	@Resource
	protected SafeManager safeManager;
	
	@Resource
	protected Functions functions;
	
	@Resource
	protected BeanIntrospector beanIntrospector;
	
	public String json(Object model) {
		return json(model, null, false, "execute", false);
	}
	
	/**
	 * 
	 * 
	 * @param model
	 * @param properties: list all the properties to populate (support the nested property)
	 * @param applyToChildModel: also populate the child model property, NOTICE: be careful to avoid the dead cycle
	 * @param permissionCheckName: the name of the 'model'
	 * @return
	 */
	public String json(Object model, String properties, boolean applyToChildModel, String permissionCheckName, boolean treatExFuncAsWholeProperty) {
		String json;
		try {
			JsonContext jsonContext = new JsonContext();
			jsonContext.root = model;
			jsonContext.applyToChildModel = applyToChildModel;
			// security information
			if (permissionCheckName != null && permissionCheckName.length() > 0) {
				HttpServletRequest request = ServletActionContext.getRequest();
				UserInfo user = safeManager.getLoginUser(request);
				// permission filter check the page permission, user is null means the page field doesn't need permission check
				if (user != null) {
					ValueStack stack = ServletActionContext.getValueStack(request);
					Context secContext = new Context(stack, functions);
					secContext.setUserInfo(user);
					jsonContext.secContext = secContext;
					jsonContext.path = request.getServletPath();
				}
			}
			
			if (properties == null || properties.length() == 0) {
				json = json(model, jsonContext, null, permissionCheckName).toString();
			} else {
				Map<String, Map> group = groupProperties(StringUtils.split(properties, ','), treatExFuncAsWholeProperty);
				Object jsonObject = json(model, jsonContext, group, permissionCheckName);
				json = jsonObject.toString();
			}

			// refer to isStandardProperty
			if (model instanceof String || model instanceof Date || model instanceof Character 
					|| model != null && model.getClass().isEnum()) {
				json = "\"" + json + "\"";
			}
		} catch (JSONException ex) {
			logger.error("convert json object", ex);
			json = "";
		}
		return json;
	}
	
	private class JsonContext {
		Context secContext;
		Object root;
		boolean applyToChildModel;
		String path;
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private Object json(Object model, JsonContext jsonContext, Map<String, Map> group, String fullFieldName) throws JSONException {
		Object value = null;
		if (model == null && (group == null || group.size() == 0)) {
			value = NULL;
        } else {
        	boolean createJSONObj = false;
        	if (model != null) {
        		Class modelClass = model.getClass();
	        	if (model instanceof Collection) {
	        		JSONArray jsonObject = new JSONArray();
		        	Collection<Object> coll = (Collection<Object>) model;
		        	int index = 0;
		        	for (Object item : coll) {
		        		jsonObject.put(index++, json(item, jsonContext, group, fullFieldName));	// don't use the extra name
		        	}
		        	value = jsonObject;
	        	} else if (modelClass.isArray()) {
		        	JSONArray jsonObject = new JSONArray();
		        	int len = Array.getLength(model);
		        	for (int i = 0; i < len; i++) {
		        		Object item = Array.get(model, i);
		        		jsonObject.put(i, json(item, jsonContext, group, fullFieldName));
		        	}
		        	value = jsonObject;
	        	} else if (model instanceof Enum) {
	        		Enum enumObj = (Enum)model;
	        		value = ognlExUtil.convertValue(enumObj, String.class, fullFieldName);
	        	} else if (model instanceof Map) {
	        		createJSONObj = true;
		        } else if (isStandardProperty(modelClass) 
		        		|| modelClass.getName().startsWith("java.") 
		        		|| modelClass.getClassLoader() == null) {
		        	if (group instanceof MappingProperty) {
						String mapping = ((MappingProperty)group).getMapping();
						value = executeFunc(jsonContext.root, mapping, model);
					} else if (model instanceof Date) {
		        		value = ognlExUtil.convertValue(model, String.class, fullFieldName);
		        	} else {
		        		value = model;
		        	}
		        } else {
		        	createJSONObj = true;
		        }
        	} else {
        		createJSONObj = true;
        	}
        	
        	if (createJSONObj) { 
        		if (group != null && group.size() > 0) {
		        	JSONObject jsonObject = new JSONObject();
		        	
		        	for (String property : group.keySet()) {
		        		Object propertyValue;
		        		if (model != null) {
	        				propertyValue = functions.val(model, property, jsonContext.secContext, jsonContext.path, fullFieldName);
		        		} else {
		        			propertyValue = null;
		        		}
		        		
		        		Map<String, Map> populateProperties;
			        	if (jsonContext.applyToChildModel && model != null) {
			        		// populate current property and the applied property
			        		populateProperties = new HashMap<String, Map>();
			        		populateProperties.putAll(group);
			        		Map ret = group.get(property);
			        		if (ret != null) {
			        			populateProperties.putAll(ret);
			        		}
			        	} else {
			        		populateProperties = group.get(property);
			        	}
						jsonObject.putOpt(property, json(propertyValue, jsonContext, populateProperties, fullFieldName + '.' + property));
		        	}
		        	value = jsonObject;
        		} else {
        			if (model instanceof Map) {
        				value = new JSONObject((Map)model);
        			} else {
        				// only scan the first level to avoid dead cycle
        				Map<String, Map> firstGroup = new HashMap<String, Map>();
        				ModelInfo modelInfo = beanIntrospector.getModelInfo(model.getClass().getName());
        				for (PropertyInfo propertyInfo : modelInfo.getAllProperties()) {
        					if (propertyInfo.isSimpleObj()) {
        						firstGroup.put(propertyInfo.getName(), null);
        					}
        				}
        				if (firstGroup.size() > 0) {
        					value = json(model, jsonContext, firstGroup, fullFieldName);
        				} else {
        					value = NULL;
        				}
        			}
        		}
	        }
        }
		return value;
	}
	
	// Refer to functions.val()
	private Object executeFunc(Object root, String property, Object value) {
		String funcName = null;
		String[] args = null;
		
		int index = property.indexOf('.', 1);
		if (index > 0) {
			funcName = property.substring(1, index);
			int end = property.indexOf('(', index + 1);
			if (end == -1) {
				property = property.substring(index + 1);
			} else {
				args = StringUtils.split(property.substring(end + 1, property.length() - 1), ',');
				property = property.substring(index + 1, end);
			}
		}
		
		FunctionCallback callback = functions.getExFunction(funcName);
		Object val = callback == null ? value : callback.execute(value, root, property, args);
		return val;
	}
	
	public Map<String, Map> groupProperties(String[] properties, boolean treatExFuncAsWholeProperty) {
		return StringUtil.groupProperties(properties, treatExFuncAsWholeProperty);
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private boolean isStandardProperty(Class clazz) {
        return clazz.isPrimitive()                  ||
        	clazz.isAssignableFrom(String.class)    ||
            clazz.isAssignableFrom(Number.class)	||
            clazz.isAssignableFrom(Character.class) ||
            clazz.isAssignableFrom(Boolean.class)	||
            clazz.isAssignableFrom(Date.class);
    }
	
	public Object parse(String str, Class genericClazz) {
		if (str == null || str.isEmpty()) {
			return str;
		}
		
		Object ret = null;
		try {
			if (str.charAt(0) == '[') {
				List list = new ArrayList();
				ret = list;
				JSONArray jsonObj = new JSONArray(str);
				
				try {
					List<JSONObject> jsonArray = convertJSONArray(jsonObj);
					
					if (Map.class.isAssignableFrom(genericClazz)) {
						for (JSONObject jsonObject : jsonArray) {
							Map<String, String> map = new HashMap<String, String>();
							convert(jsonObject, map);
							list.add(map);
						}
					} else {
						for (int i = 0, len = jsonArray.size(); i < len; i++) {
							list.add(ModelUtil.newInstance(genericClazz));
						}
						convert(jsonArray, list, genericClazz);
					}
				} catch (JSONException ex) {
					int len = jsonObj.length();
					for (int i = 0; i < len; i++) {
						list.add(jsonObj.get(i));
					}
				}
			} else {
				ret = ModelUtil.newInstance(genericClazz);
				JSONObject jsonObj = new JSONObject(str);
				convert(jsonObj, ret, null);
			}
		} catch (JSONException ex) {
			logger.error("Wrong JSON data", ex);
		}
		return ret;
	}
	
	private List<JSONObject> convertJSONArray(JSONArray jsonArray) throws JSONException {
		int len = jsonArray.length();
		List<JSONObject> destPropertyVal = new ArrayList<JSONObject>(len);
		for (int i = 0; i < len; i++) {
			destPropertyVal.add(jsonArray.getJSONObject(i));
		}
		return destPropertyVal;
	}

	// TODO: fix defect
	private void convert(Object jsonObject, Object model, final Class defaultGenericType) {
		Map<Object, Object> identityMap = new IdentityHashMap<Object, Object>();
		ScannerCallback callback = new ScannerCallback() {
			@Override
			public ScannerFlag proceed(ScannerContext scannerContext) {
				try {
					PropertyInfo propertyInfo = scannerContext.getPropertyDescriptor();
					if (scannerContext.isCollection()) {
						JSONObject json = (JSONObject) scannerContext.getDestModel();
						JSONArray jsonArray = json.getJSONArray(propertyInfo.getName());
						int len = jsonArray.length();
						List<Object> propertyValue = new ArrayList<Object>(len);
						List<JSONObject> destPropertyVal = new ArrayList<JSONObject>(len);
						Class genericType = propertyInfo.getGenericTypeClass();
						if (genericType.equals(Object.class)) {
							genericType = defaultGenericType;
						}
						for (int i = 0; i < len; i++) {
							propertyValue.add(ModelUtil.newInstance(genericType));
							destPropertyVal.add(jsonArray.getJSONObject(i));
						}
						scannerContext.setDestPropertyValueWithoutModify(destPropertyVal);
						scannerContext.setSrcPropertyValue(propertyValue);
					} else if (!scannerContext.isSimpleObj()) {
						JSONObject json = (JSONObject) scannerContext.getDestModel();
						JSONObject destPropertyVal = json.getJSONObject(propertyInfo.getName());
						scannerContext.setDestPropertyValueWithoutModify(destPropertyVal);
						Object propertyValue = ModelUtil.newInstance(propertyInfo.getTypeClass());
						scannerContext.setSrcPropertyValue(propertyValue);
					} else {
						JSONObject json = (JSONObject) scannerContext.getDestModel();
						Object propertyVal = getSimpleValue(json, propertyInfo.getName(), propertyInfo.getTypeClass());
						scannerContext.setDestPropertyValueWithoutModify(propertyVal);
						scannerContext.setSrcPropertyValue(propertyVal);
					}
					return ScannerFlag.SCANCHILD;
				} catch (JSONException ex) {
					logger.error("Failed to parse the json string", ex);
					return ScannerFlag.STOP;
				}
			}
			
			private Object getSimpleValue(JSONObject json, String property, Class type) throws JSONException {
				Object ret = null;
				if (type.isAssignableFrom(Boolean.class)) {
					ret = json.optBoolean(property);
				} else if (type.isAssignableFrom(Double.class)) {
					ret = json.optDouble(property);
				} else if (type.isAssignableFrom(Integer.class)) {
					ret = json.optInt(property);
				} else if (type.isAssignableFrom(Long.class)) {
					ret = json.optLong(property);
				} else if (type.isAssignableFrom(String.class)) {
					ret = json.optString(property);
				}
				return ret;
			}
		};
		ModelScanner.getInstance().scan(model, jsonObject, null, callback, identityMap);
	}
	
	private void convert(JSONObject jsonObject, Map<String, String> model) {
		Iterator<String> nameItr = jsonObject.keys();
		while(nameItr.hasNext()) {
			String name = nameItr.next();
		    try {
		    	model.put(name, jsonObject.getString(name));
			} catch (JSONException ex) {
				logger.error("Failed to get the json string", ex);
			}
		}
	}
}